#include "../config.h"

#include <sys/types.h>  // waitpid/kill
#include <sys/wait.h>   // waitpid
#include <signal.h>     // kill
#include <string.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <glibmm/i18n.h>
#include <boost/date_time/posix_time/posix_time.hpp>

#include "log.h"
#include "application.h"
#include "mainwindow.h"
#include "processview.h"
#include "format.h"
#include "strfuncs.h"
#include "yellowpage.h"
#include "command.h"
#include "paths.h"

#define EXEC_ENV "EXEC_FROM_GPYP3"

namespace gpyp {

//========================
// Process
void Process::spawn_setup(gpointer /*data*/) {
  setsid();

  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);
  setenv(EXEC_ENV, "YES", 0);
}

gboolean Process::_on_stdout(GIOChannel  */*source*/,
                             GIOCondition condition,
                             Process     *proc) {
  if (condition == G_IO_IN) {
    proc->on_read_buf(proc->ioc_stdout_,
                      proc->buf_stdout_,
                      proc->signal_stdout_);
  }
  return TRUE;
}
gboolean Process::_on_stderr(GIOChannel  */*source*/,
                             GIOCondition condition,
                             Process     *proc) {
  if (condition == G_IO_IN) {
    proc->on_read_buf(proc->ioc_stderr_,
                      proc->buf_stderr_,
                      proc->signal_stderr_);
  }
  return TRUE;
}
void Process::on_read_buf(GIOChannel   *source,
                          std::string  &buffer,
                          SignalOutput &signal) {
  static gchar buf_c[32];
  while(1) {
    gsize size = 0;
    if (g_io_channel_read_chars(source, buf_c, sizeof(buf_c), &size, NULL) != G_IO_STATUS_ERROR && size) {
      char *s = buf_c, *p = buf_c, *e = buf_c + size;
      for(; p < e; ++p) {
        if (*p == '\0') *p = ' ';
        if (*p == '\n') {
          buffer.append(s, p + 1);
          signal(buffer);
          buffer.clear();
          s = p + 1;
        }
      }
      if (s < e) buffer.append(s, e);
    } else {
      break;
    }
  }
}

void Process::_on_dead(GPid /*pid*/, gint status, gpointer data) {
  Process *self = (Process *)data;
  self->status_ = status;
  self->on_dead();
  //Glib::signal_idle().connect(sigc::mem_fun(*self, &Process::on_dead));
}
bool Process::on_dead() {
  g_spawn_close_pid(pid_);
  clear_data(false);
  time_end_ = boost::posix_time::second_clock::local_time();
  signal_dead_();
  return false;
}

void Process::on_error(const Glib::ustring &message) {
  error_message_ = message;
  signal_error_();
}

void Process::clear_data(bool all) {
  pid_ = 0;

#define CLEAR_WATCH(NAME) {                                     \
    if (watch_##NAME##_) g_source_remove(watch_##NAME##_);      \
    watch_##NAME##_ = 0;                                        \
  }
  CLEAR_WATCH(proc);
  CLEAR_WATCH(stdout);
  CLEAR_WATCH(stderr);
#undef CLEAR_WATCH

  fd_stdin_ = fd_stdout_ = fd_stderr_ = 0;
#define CLEAR_IOC(NAME) {                                       \
    if (ioc_##NAME##_) {                                        \
      g_io_channel_shutdown(ioc_##NAME##_, FALSE, NULL);        \
      g_io_channel_unref(ioc_##NAME##_);                        \
    }                                                           \
    ioc_##NAME##_ = NULL;                                       \
  }
  CLEAR_IOC(stdout);
  CLEAR_IOC(stderr);
#undef CLEAR_IOC

  buf_stdout_.clear();
  buf_stderr_.clear();

  if (all) {
    status_   = 0;
    signaled_ = false;
  }
}

bool Process::run() {
  if (alive()) {
    on_error("Running");
    return false;
  }

  ConfMain::RefGeneral general = app_.conf().general();
  const Glib::ustring &shell = *general->shell();
  gint     shell_argc = 0;
  gchar  **shell_argv = NULL;

  {
    GError  *err        = NULL;
    gboolean parse_res  = g_shell_parse_argv(shell.c_str(),
                                             &shell_argc,
                                             &shell_argv,
                                             &err);
    if (!parse_res) {
      if (shell_argv) g_strfreev(shell_argv);

      Glib::ustring err_str = _("failed to parse shell command");
      if (err && err->message) {
        err_str += " - ";
        err_str += err->message;
      }
      on_error(err_str);
    }
    if (err) g_error_free(err);
    err = NULL;
    if (!parse_res) return false;
  }

  {
    gchar **shell_argv_new =
      (gchar **)g_malloc((shell_argc + 2) * sizeof(gchar *));
    for(int i = 0; i < shell_argc; ++i) {
      shell_argv_new[i] = g_strdup(shell_argv[i]);
    }
    shell_argv_new[shell_argc] =
      g_strconcat((*general->command_prefix()).c_str(),
                  command_.c_str(),
                  NULL);
    shell_argv_new[shell_argc + 1] = NULL;
    g_strfreev(shell_argv);
    shell_argv = shell_argv_new;
  }

  clear_data(true);
  {
    gboolean res = FALSE;
    GError  *err = NULL;
    res = g_spawn_async_with_pipes(NULL, /* working_directory */
                                   shell_argv,
                                   NULL, /* envp */
                                   G_SPAWN_DO_NOT_REAP_CHILD,
                                   &Process::spawn_setup,
                                   this,
                                   &pid_,
                                   &fd_stdin_,
                                   &fd_stdout_,
                                   &fd_stderr_,
                                   &err);
    g_strfreev(shell_argv);

    if (res) {
      time_start_ = boost::posix_time::second_clock::local_time();
      time_end_   = boost::posix_time::not_a_date_time;

      watch_proc_ = g_child_watch_add(pid_, &Process::_on_dead, this);

      const gchar *enc = NULL;
      g_get_charset(&enc);
      if (enc && strcmp(enc, "UTF-8") == 0) enc = NULL;

#define INIT_IOC(NAME) {                                                \
        ioc_##NAME##_ = g_io_channel_unix_new(fd_##NAME##_);            \
        g_io_channel_set_flags(ioc_##NAME##_, G_IO_FLAG_NONBLOCK, NULL); \
        if (enc) {                                                      \
          g_io_channel_set_encoding(ioc_##NAME##_, enc, NULL);          \
        } else {                                                        \
          g_io_channel_set_encoding(ioc_##NAME##_, NULL, NULL);         \
          g_io_channel_set_buffered(ioc_##NAME##_, FALSE);              \
        }                                                               \
        watch_##NAME##_ =                                               \
          g_io_add_watch(ioc_##NAME##_, G_IO_IN,                        \
                         (GIOFunc)&Process::_on_##NAME, this);          \
      }
      INIT_IOC(stdout);
      INIT_IOC(stderr);
#undef INIT_IOC

      signal_run_();
    } else {
      Glib::ustring err_str = _("failed to run process");
      if (err && err->message) {
        err_str += " - ";
        err_str += err->message;
      }
      on_error(err_str);
    }
    if (err) g_error_free(err);
    err = NULL;
    return res;
  }
}

void Process::terminate() {
  if (alive()) {
    signaled_ = true;
    log::log << format("%s(%i): terminate", name_.c_str(), pid_) << log::endl;
    ::kill(-pid_, SIGTERM);
  }
}
void Process::kill() {
  if (alive()) {
    signaled_ = true;
    log::log << format("%s(%i): kill", name_.c_str(), pid_) << log::endl;
    ::kill(-pid_, SIGKILL);
  }
}
void Process::signal(int sig) {
  if (alive()) {
    signaled_ = true;
    log::log << format("%s(%i): signal %i", name_.c_str(), pid_, sig) << log::endl;
    ::kill(-pid_, sig);
  }
}

bool Process::status_exited() {
  return WIFEXITED(status_);
}
int Process::status_exitstatus() {
  return WEXITSTATUS(status_);
}
bool Process::signaled() {
  return WIFSIGNALED(status_);
}
bool Process::signal_sent() {
  return signaled_;
}
bool Process::aborted() {
  return (!signal_sent() &&
          (!status_exited() ||
           status_exitstatus() != 0 ||
           signaled()));
}

boost::posix_time::time_duration Process::get_uptime() {
  if (time_start_.is_not_a_date_time()) {
    return boost::posix_time::not_a_date_time;
  }
  boost::posix_time::ptime e = time_end_;
  if (e.is_not_a_date_time()) {
    e = boost::posix_time::second_clock::local_time();
  }
  return e - time_start_;
}

Process::Process(Application         &app,
                 ProcessType          type,
                 const Glib::ustring &command,
                 const Glib::ustring &name,
                 RefChannel           channel) :
  app_         (app),
  type_        (type),
  command_     (command),
  channel_     (channel),
  pid_         (0),
  status_      (0),
  signaled_    (false),
  watch_proc_  (0),
  fd_stdin_    (0),
  fd_stdout_   (0),
  fd_stderr_   (0),
  ioc_stdout_  (NULL),
  ioc_stderr_  (NULL),
  watch_stdout_(0),
  watch_stderr_(0)
{
  const char *slash = strrchr(name.c_str(), '/');
  name_.assign(slash ? slash + 1 : name.c_str());
}
Process::~Process() {
  clear_data(true);
}

//========================
// Recorder
bool Recorder::create_filename() {
  CommandRunner &cr = app_.commandrunner();
  ConfMain::RefRecord record = app_.conf().record();
  CommandOpts opts;
  opts.ch = ch_;

  Glib::ustring filename;
  if (!cr.format(*record->filename(), filename, &opts) ||
      filename.empty()) {
    log::err << "Record: " << _("failed to format filename") << log::endl;
    return false;
  }

  try {
    Glib::ustring dir;
    Glib::ustring name;
    const char *slash = strrchr(filename.c_str(), '/');
    if (slash) {
      dir.assign(filename.c_str(), slash);
      name = slash + 1;
    } else {
      dir = ".";
      name = filename;
    }
    size_t i = 1;
    while(Glib::file_test(Glib::filename_from_utf8(filename),
                          Glib::FILE_TEST_EXISTS)) {
      if (i == 100) {
        filename.erase();
        break;
      }
      Glib::ustring name_new = name, suffix = Glib::ustring("_") + tostr(i);
      Glib::ustring::size_type pos = name_new.rfind('.');
      if (pos == Glib::ustring::npos) {
        name_new += suffix;
      } else {
        name_new.insert(pos, suffix);
      }
      filename = Glib::build_filename(dir, name_new);
      ++i;
    }
    if (filename.empty()) {
      log::err << "Record: " << _("failed to generate filename") << log::endl;
      return false;
    }

    std::string dir_e = Glib::filename_from_utf8(dir);
    if (!Glib::file_test(dir_e, Glib::FILE_TEST_IS_DIR)) {
      if (g_mkdir_with_parents(dir_e.c_str(), 0755) < 0) {
        log::err << "Record: " << _("failed to create directory") << log::endl;
        return false;
      }
    }
  } catch(const Glib::ConvertError &ex) {
    log::err << "Record: " << ex.what() << log::endl;
    return false;
  }
  filename_ = filename;
  return true;
}

void Recorder::write_log(const std::string &message) {
  if (log_fd_ > 0) {
    if (write(log_fd_, message.c_str(), message.size()) > 0) {
#ifdef __linux__
      fdatasync(log_fd_);
#else
      fsync(log_fd_);
#endif
    }
  }
}

void Recorder::proc_on_run() {
  write_log(format("> %s (%i)\n", proc_->get_command().c_str(), proc_->get_pid()));
}
void Recorder::proc_on_error() {
  log::err << "Record: " << _("failed to run process") << log::endl;
  write_log(_("> Error! Failed to run process.\n"));
  time_started_ = 0;
}
void Recorder::proc_on_stdout(const std::string &message) {
  write_log(message);
}
void Recorder::proc_on_stderr(const std::string &message) {
  write_log(message);
}
void Recorder::proc_on_dead() {
  write_log(proc_->signal_sent()
            ? "> Signaled\n"
            : (proc_->status_exited()
               ? format("> Returned %i\n", proc_->status_exitstatus()).c_str()
               : "> Aborted\n"));

  conn_run_   .disconnect();
  conn_error_ .disconnect();
  conn_stdout_.disconnect();
  conn_stderr_.disconnect();
  conn_dead_  .disconnect();
  Glib::signal_idle().connect(sigc::mem_fun(*this, &Recorder::proc_on_dead_idle));
}
bool Recorder::proc_on_dead_idle() {
  ConfMain::RefRecord record = app_.conf().record();
  if (!filename_.empty() && *record->rm_zero_file() &&
      Glib::file_test(filename_, Glib::FILE_TEST_IS_REGULAR)) {
    struct stat s;
    memset(&s, 0, sizeof(s));
    if (g_stat(filename_.c_str(), &s) == 0 && s.st_size == 0) {
      log::log << "Record: " << format(_("remove - %s"), filename_.c_str())
               << log::endl;
      g_remove(filename_.c_str());
    }
  }

  if (proc_->signal_sent()) {
    log::log << "Record: " << _("signaled") << log::endl;
    dead_count_ = 0;
    on_stop();
    return false;
  }
  int count = *record->retry_count();
  if (static_cast<unsigned>(time(NULL) - time_started_) < *record->retry_timeout()) {
    ++dead_count_;
  } else {
    dead_count_ = 0;
  }
  bool retry = count < 0 || (count > 0 && dead_count_ <= static_cast<unsigned>(count));
  if (!retry && *record->retry_ch_exists()) {
    RefYellowPage yp = app_.ypcollection()->find(ch_->get_yp());
    if (yp && yp->find(ch_->get_id())) retry = true;
  }
  if (retry) {
    log::log << "Record: " << _("process dead. restarting...") << log::endl;
    if (run_new_process()) {
      stopped_ = false;
      return false;
    }
  } else {
    log::log << "Record: " << _("process dead") << log::endl;
  }
  on_stop();
  return false;
}
bool Recorder::run_new_process() {
  log::dbg << "Recorder::run_new_process" << log::endl;
  CommandRunner &cr = app_.commandrunner();
  ConfMain::RefRecord record = app_.conf().record();
  CommandOpts opts;
  opts.ch = ch_;
  opts.filename = filename_.c_str();
  Glib::ustring command;
  if (!cr.format(*record->command(), command, &opts) || command.empty()) {
    log::err << "Record: " << _("failed to format command") << log::endl;
    return false;
  }
  conn_run_   .disconnect();
  conn_error_ .disconnect();
  conn_stdout_.disconnect();
  conn_stderr_.disconnect();
  conn_dead_  .disconnect();
  proc_.reset(new Process(app_, PROCESS_TYPE_RECORD, command, _("Record"), ch_));
  if (log_fd_) {
    conn_run_    = proc_->signal_run()   .connect(sigc::mem_fun(*this, &Recorder::proc_on_run));
    conn_stdout_ = proc_->signal_stdout().connect(sigc::mem_fun(*this, &Recorder::proc_on_stdout));
    conn_stderr_ = proc_->signal_stderr().connect(sigc::mem_fun(*this, &Recorder::proc_on_stderr));
  }
  conn_error_ = proc_->signal_error().connect(sigc::mem_fun(*this, &Recorder::proc_on_error));
  conn_dead_  = proc_->signal_dead() .connect(sigc::mem_fun(*this, &Recorder::proc_on_dead));
  time_started_ = time(NULL);
  signal_new_process_(proc_);
  return proc_->run();
}

void Recorder::on_stop() {
  log::log << "Record: " << _("Stopped") << log::endl;
  stopped_ = true;
  signal_stop_();
}

void Recorder::start() {
  if (proc_ || !create_filename()) return;

  if (log_fd_ == 0 && *app_.conf().record()->save_log()) {
    Glib::ustring dir = Glib::path_get_dirname(filename_);

    CommandRunner &cr = app_.commandrunner();
    Glib::ustring  filename;
    CommandOpts    opts;
    opts.dir = dir.c_str();
    opts.ch  = ch_;
    if (cr.format(*app_.conf().record()->log_filename(), filename, &opts)) {
      try {
        std::string path = Glib::filename_from_utf8(filename);
        log_fd_ = open(path.c_str(), O_WRONLY | O_CREAT, 0644);
        if (log_fd_ < 0) {
          log::err << "Record: " << format(_("failed to open log file - %s"), path.c_str()) << log::endl;
          log_fd_ = 0;
        }
      } catch(const Glib::ConvertError &ex) {
        log::err << "Record: " << ex.what() << log::endl;
      }
    } else {
      log::err << "Record: " << _("failed to format log filename") << log::endl;
    }
  }

  if (run_new_process()) {
    log::log << "Record: " << _("Started") << log::endl;
    stopped_ = false;
  } else {
    on_stop();
  }
}

Recorder::Recorder(Application &app, RefChannel ch) :
  app_         (app),
  ch_          (ch),
  log_fd_      (0),
  stopped_     (true),
  time_started_(0),
  dead_count_  (0)
{
}
Recorder::~Recorder() {
  conn_run_   .disconnect();
  conn_error_ .disconnect();
  conn_stdout_.disconnect();
  conn_stderr_.disconnect();
  conn_dead_  .disconnect();
  if (log_fd_) {
    fsync(log_fd_);
    close(log_fd_);
    log_fd_ = 0;
  }
}

//========================
// BuiltinCommand
void BuiltinCommand::enum_commands_hash(const char */*name*/, BuiltinCommand *value, EnumData *data) {
  if (!value->cb_cmd_.empty()) {
    data->slot(data->prefix + value->name_, *value);
  } else {
    EnumData data_n;
    data_n.prefix = data->prefix + value->name_ + ".";
    data_n.slot   = data->slot;
    g_hash_table_foreach(value->bc_hash_, (GHFunc)&BuiltinCommand::enum_commands_hash, &data_n);
  }
}
void BuiltinCommand::value_destroy_func(BuiltinCommand *value) {
  delete value;
}

#define ADD_BC(...) {                                                   \
    if (name.empty()) {                                                 \
      log::err << "Built-in command name empty" << log::endl;           \
    } else if (g_hash_table_lookup(bc_hash_, name.c_str())) {           \
      log::err << "Built-in command name duplicated - "                 \
               << name << log::endl;                                    \
    } else {                                                            \
      BuiltinCommand *bc = new BuiltinCommand(__VA_ARGS__);             \
      g_hash_table_insert(bc_hash_, (gpointer)bc->name_.c_str(), bc);   \
      return bc;                                                        \
    }                                                                   \
    return NULL;                                                        \
  }
BuiltinCommand *BuiltinCommand::add_ns(const Glib::ustring &name) {
  ADD_BC(name);
}
BuiltinCommand *BuiltinCommand::add_cmd(const Glib::ustring &name,
                                        CMDSlot              cb_cmd,
                                        const Glib::ustring &help_string) {
  ADD_BC(name, cb_cmd, help_string);
}

void BuiltinCommand::remove(BuiltinCommand &bc) {
  if (!g_hash_table_remove(bc_hash_, bc.name_.c_str())) {
    log::err << "failed to remove builtin command" << log::endl;
  }
}

bool BuiltinCommand::run(VariableName &name,
                         ShellLine    &line,
                         CommandOpts  &opts) const {
  const Glib::ustring *cname = NULL;
  if (name.get(cname, false)) {
    BuiltinCommand *bc = (BuiltinCommand *)g_hash_table_lookup(bc_hash_, cname->c_str());
    if (bc) {
      name.push();
      return bc->run(name, line, opts);
    }
  }
  if (!name && cb_cmd_) return cb_cmd_(line, opts);
  return false;
}

void BuiltinCommand::enum_commands(EnumSlot slot) {
  EnumData data;
  data.slot = slot;
  g_hash_table_foreach(bc_hash_, (GHFunc)&BuiltinCommand::enum_commands_hash, &data);
}

BuiltinCommand::BuiltinCommand() :
  bc_hash_(create_hash_table())
{
}
BuiltinCommand::BuiltinCommand(const Glib::ustring &name) :
  name_   (name),
  bc_hash_(create_hash_table())
{
}
BuiltinCommand::BuiltinCommand(const Glib::ustring &name,
                               CMDSlot              cb_cmd,
                               const Glib::ustring &help_string) :
  name_       (name),
  cb_cmd_     (cb_cmd),
  help_string_(help_string),
  bc_hash_    (create_hash_table())
{
}
BuiltinCommand::~BuiltinCommand() {
  if (bc_hash_) g_hash_table_destroy(bc_hash_);
}

//========================
// PromptDialog
class PromptDialog : public Gtk::Dialog {
private:
  Gtk::Entry entry_;

  void entry_on_activate() {
    response(Gtk::RESPONSE_OK);
  }

public:
  Glib::ustring run() {
    Gtk::Dialog::run();
    return entry_.get_text();
  }

  PromptDialog(const VariableName& var) {
    Glib::ustring def;
    var.get_arg(0, def);
    entry_.set_text(def);
    entry_.signal_activate().connect(sigc::mem_fun(*this, &PromptDialog::entry_on_activate));
    get_vbox()->pack_end(entry_);
    set_title(PACKAGE_NAME);
    add_button(Gtk::Stock::OK, Gtk::RESPONSE_OK);
    show_all();
  }
};

//========================
// Commandrunner
void CommandRunner::shell_escape(Glib::ustring &str) {
  const Glib::ustring &esc = *app_.conf().general()->shell_esc();
  const char *esc_c = esc.c_str();
  char buf[2] = {'\\', '\0'};
  Glib::ustring res;
  const char *l = str.c_str();
  for(const char *c = l; *c; ++c) {
    if (strchr(esc_c, *c)) {
      res.append(l, c);
      buf[1] = *c;
      res.append(buf, 2);
      l = c + 1;
    }
  }
  res.append(l);
  str.swap(res);
}

bool CommandRunner::test_pid(const Glib::ustring &name) {
  pid_t pid = load_pid(app_.conf().get_dir_path(), name);
  if (pid) {
    std::string proc = "/proc/" + tostr(pid);
    std::string environ = proc + "/environ";
    if (Glib::file_test(environ, Glib::FILE_TEST_IS_REGULAR)) {
      char  *data = NULL;
      gsize  length;
      bool   match = false;
      if (g_file_get_contents(environ.c_str(), &data, &length, NULL)) {
        char *name_s = data;
        for(gsize i = 0; i < length; ++i) {
          if (data[i] == '=') {
            if (std::string(name_s, data + i) == EXEC_ENV) {
              match = true;
              break;
            }
            while(data[i] && i < length) ++i;
            name_s = data + i + 1;
          }
        }
      }
      if (data) g_free(data);
      if (match) return true;
    } else {
      return Glib::file_test(proc, Glib::FILE_TEST_EXISTS);
    }
  }
  return false;
}

bool CommandRunner::main_on_quit() {
  if (*app_.conf().record()->term_on_exit()) {
    bool kill = *app_.conf().record()->kill_on_exit();
    for(RecorderList::iterator iter = recorders_.begin(); iter != recorders_.end(); ++iter) {
      RefProcess proc = iter->recorder_->get();
      if (proc) {
        if (kill) {
          proc->kill();
        } else {
          proc->terminate();
        }
      }
    }
  }
  return true;
}

#define RUN_SPECIAL_PROCESS(NAME, TYPE, CHECK) {                        \
    if ((!(CHECK) || !test_pid(#NAME)) &&                               \
        (!proc_##NAME##_ || !proc_##NAME##_->alive())) {                \
      Glib::ustring command;                                            \
      if (format(*app_.conf().command()->NAME()->command(),             \
                 command, NULL)) {                                      \
        log::log << "> " << command << log::endl;                       \
        proc_##NAME##_.reset(new Process(app_, PROCESS_TYPE_SPECIAL_##TYPE, command, #NAME)); \
        proc_##NAME##_->signal_dead()                                   \
          .connect(sigc::mem_fun(*this, &CommandRunner::NAME##_on_dead)); \
        proc_##NAME##_->signal_error()                                  \
          .connect(sigc::mem_fun(*this, &CommandRunner::NAME##_on_error)); \
        signal_run_##NAME##_(proc_##NAME##_);                           \
        if (proc_##NAME##_->run()) {                                    \
          save_pid(app_.conf().get_dir_path(),                          \
                   #NAME, proc_##NAME##_->get_pid());                   \
        }                                                               \
      }                                                                 \
    }                                                                   \
  }
void CommandRunner::run_peercast(bool check_prev) {
  RUN_SPECIAL_PROCESS(peercast, PEERCAST, check_prev);
}
void CommandRunner::run_pcraw_proxy(bool check_prev) {
  RUN_SPECIAL_PROCESS(pcraw_proxy, PCRAW_PROXY, check_prev);
}
#undef RUN_SPECIAL_PROCESS

void CommandRunner::on_aborted(RefProcess proc) {
  signal_aborted_(proc);
}

#define CHECK_SPECIAL_PROCESS_ABORTED(NAME) {               \
    remove_pid(app_.conf().get_dir_path(), #NAME);          \
    if (proc_##NAME##_->aborted() &&                        \
        proc_##NAME##_->get_uptime().total_seconds() > 3) { \
      run_##NAME();                                         \
    }                                                       \
  }
void CommandRunner::peercast_on_dead() {
  process_dead_message(proc_peercast_);
  CHECK_SPECIAL_PROCESS_ABORTED(peercast);
}
void CommandRunner::peercast_on_error() {
  process_error_message(proc_peercast_);
}
void CommandRunner::pcraw_proxy_on_dead() {
  process_dead_message(proc_pcraw_proxy_);
  CHECK_SPECIAL_PROCESS_ABORTED(pcraw_proxy);
}
void CommandRunner::pcraw_proxy_on_error() {
  process_error_message(proc_pcraw_proxy_);
}
#undef CHECK_SPECIAL_PROCESS_ABORTED

void CommandRunner::remove_process(ProcessContext *pc) {
  for(ProcessList::iterator iter = processes_.begin(); iter != processes_.end(); ++iter) {
    if (&*iter == pc) {
      processes_.erase(iter);
      return;
    }
  }
  log::err << "bug: CommandRunner::remove_process" << log::endl;
}
void CommandRunner::remove_recorder(RecorderContext *rc) {
  for(RecorderList::iterator iter = recorders_.begin(); iter != recorders_.end(); ++iter) {
    if (&*iter == rc) {
      recorders_.erase(iter);
      return;
    }
  }
  log::err << "bug: CommandRunner::remove_recorder" << log::endl;
}

void CommandRunner::process_dead_message(RefProcess proc) {
  if (proc->aborted()) {
    log::err << gpyp::format(_("> %s aborted"), proc->get_name().c_str()) << log::endl;
    on_aborted(proc);
  } else {
    log::log << gpyp::format(_("> %s ended"), proc->get_name().c_str()) << log::endl;
  }
}
void CommandRunner::process_error_message(RefProcess proc) {
  log::err << "> " << proc->get_name() << " " << proc->get_error_message() << log::endl;
}
void CommandRunner::process_on_dead(ProcessContext *pc) {
  process_dead_message(pc->proc_);
  remove_process(pc);
}
void CommandRunner::process_on_error(ProcessContext *pc) {
  process_error_message(pc->proc_);
  remove_process(pc);
}

void CommandRunner::recorder_on_stop(RecorderContext *rc) {
  remove_recorder(rc);
}

bool CommandRunner::format(const Glib::ustring &command,
                           Glib::ustring       &res,
                           CommandOpts         *opts) {
  res.clear();
  const char* l = command.c_str();
  for(const char* c = l; *c; ++c) {
    switch(*c) {
    case '%':
    case '#':
      if (c[1] == '{') {
        bool escape = *c == '#';
        //bool escape = true;

        res.append(l, c);
        for(l = c + 2; *l && *l != '}'; ++l) ;
        Glib::ustring key(c, l + 1);
        VariableName var(key, 2, 1);

        ConfMain::RefNetwork net = app_.conf().network();
        if (var.match("Network", false)) {
          if (var.match("peercastsrv")) {
            key = *net->peercast_server();
          } else if (var.match("pcrawsrv")) {
            key = *net->pcraw_proxy_server();
          } else if (var.match("pcrawsrv_host")) {
            key = net->pcraw_proxy_server()->get_host();
          } else if (var.match("pcrawsrv_port")) {
            key = tostr(net->pcraw_proxy_server()->get_port());
          }
        } else if (var.match("YP", false)) {
          if (var.match("chcount")) {
            key = tostr(app_.ypcollection()->get_chcount());
          } else if (var.match("chcount_valid")) {
            key = tostr(app_.ypcollection()->get_chcount_valid());
          } else {
            YellowPage *yp = NULL;
            const Glib::ustring *ypname = NULL, *vname = NULL;
            if (var.get(ypname, vname)) {
              yp = app_.ypcollection()->find(*ypname).get();
            } else if (var.get(vname) && opts) {
              yp = opts->yp.get();
              if (!yp && opts->ch) yp = opts->ch->get_yp();
            }
            if (yp) {
              try {
                if (*vname == "name") {
                  key = *yp->name();
                } else if (*vname == "url") {
                  key = *yp->url();
                } else if (*vname == "url_base") {
                  key = URL::get_base(*yp->url());
                } else if (*vname == "url_dir") {
                  key = URL::get_dir(*yp->url());
                }
              } catch(const InvalidURL &ex) {
                log::err << ex.what() << log::endl;
                res.clear();
                return false;
              }
            }
          }
        } else if (var.match("Channel", false)) {
          if (opts && opts->ch) {
            RefChannel ch = opts->ch;
            RefFavoriteMatch fm = ch->get_favorite_match();

            Glib::ustring path;
            if (var.match("streamurl")) {
              path = "/stream/";
            } else if (var.match("playlisturl")) {
              path = "/pls/";
            } else {
              const Glib::ustring *name = NULL;
              if (var.get(name)) {
                const Channel::Property* prop =
                  Channel::get_property(name->c_str());
                if (prop) {
                  if (prop == &Channel::_prop_ypname ||
                      prop == &Channel::_prop_ypurl) {
                    using gpyp::format;
                    log::err << format("%s is deprecated. Use %c{YP.%s}.",
                                       key.c_str(),
                                       escape ? '#' : '%',
                                       prop->name)
                             << log::endl;
                  }
                  key = (ch.get()->*(prop->func_str))();
                }
              }
            }
            if (!path.empty()) {
              Glib::ustring arg("http");
              var.get_arg(0, arg);
              key = arg + "://" + *net->peercast_server() + path + ch->get_id_str() + "?tip=" + ch->get_tip();
            }
          } else {
            res.clear();
            return false;
          }
        } else if (var.match("Process", false)) {
          if (opts && opts->proc) {
            RefProcess proc = opts->proc;
            if (var.match("pid")) {
              key = tostr(proc->get_pid());
            } else if (var.match("command")) {
              key = proc->get_command();
            } else if (var.match("name")) {
              key = proc->get_name();
            }
          }
        } else if (var.match("confdir")) {
          key = app_.conf().get_dir_path();
        } else if (var.match("prompt")) {
          key = PromptDialog(var).run();
        } else if (var.match("date")) {
          Glib::ustring fmt;
          if (var.get_arg(0, fmt)) {
            typedef boost::date_time::time_facet<boost::posix_time::ptime, char> facet;
            facet *face = new facet(fmt.c_str());
            std::ostringstream  oss;
            oss.imbue(std::locale(std::locale::classic(), face));
            oss << boost::posix_time::second_clock::local_time();
            key = oss.str();
          }
        } else if (var.match("clipboard")) {
          Glib::RefPtr<Gtk::Clipboard> clip = Gtk::Clipboard::get();
          key = clip->wait_for_text();
        } else if (var.match("Path", false)) {
          if (var.match("home")) {
            key = Glib::get_home_dir();
          } else if (var.match("Gpyp", false)) {
            if (var.match("prefix")) {
              key = INSTALL_DIR_PREFIX;
            } else if (var.match("bin")) {
              key = INSTALL_DIR_BIN;
            } else if (var.match("data")) {
              key = INSTALL_DIR_DATA;
            } else if (var.match("icon")) {
              key = INSTALL_DIR_ICON;
            } else if (var.match("lib")) {
              key = INSTALL_DIR_LIB;
            } else if (var.match("applescript")) {
              key = INSTALL_DIR_APPLE_SCRIPT;
            }
          }
        } else {
          if (opts) {
            if (opts->text) {
              if (var.match("text")) {
                key = opts->text;
              } else if (var.match("textu")) {
                key = URL::encode(opts->text);
              } else if (var.match("texte")) {
                key = URL::encode(Glib::convert(opts->text, "EUC-JP", "UTF-8"));
              } else if (var.match("texts")) {
                key = URL::encode(Glib::convert(opts->text, "Shift-JIS", "UTF-8"));
              }
            }
            if (opts->command && var.match("command")) {
              key = opts->command;
            }
            if (opts->url && var.match("url")) {
              key = opts->url;
            }
            if (opts->filename && var.match("filename")) {
              key = opts->filename;
            }
            if (opts->dir && var.match("dir")) {
              key = opts->dir;
            }
          }
        }

        if (escape) {
          shell_escape(key);
        }
        res.append(key);
        c = l++;
      }
      break;
    }
  }
  res.append(l);
  return true;
}

void CommandRunner::run(const Glib::ustring &command,
                        CommandOpts         *opts,
                        ProcessType          type) {
  Glib::ustring command_fmt;
  if (!format(command, command_fmt, opts)) {
    return;
  }

  ShellLines lines;
  if (!parse_shell(command_fmt, lines)) {
    log::err << "failed to parse command" << log::endl;
    return;
  }
  for(ShellLines::iterator iter_line = lines.begin();
      iter_line != lines.end(); ++iter_line) {
    const Glib::ustring &line = iter_line->line;
    const Glib::ustring &comm = iter_line->command;
    log::log << "> " << line << log::endl;
    if (VariableName::validate(comm, '@')) {
      VariableName name(comm, 2, 1);
      CommandOpts opts_r;
      if (!opts) opts = &opts_r;

      OptionParser parser;
      Glib::ustring f_yp, f_fav, f_player;
      int f_fav_rule = -1;
      parser.add("--yp-name", f_yp);
      parser.add("--favorite-name", f_fav);
      parser.add("--favorite-rule", f_fav_rule);
      parser.add("--player", f_player);
      if (!parser.parse(*iter_line)) continue;

      if (!f_yp.empty()) {
        RefYellowPage yp = app_.ypcollection()->find(f_yp);
        if (yp) {
          opts->yp = yp;
        } else {
          log::err << "invalid yp name - " << f_yp << log::endl;
        }
      }
      if (!f_fav.empty()) {
        RefFavorite fav = app_.favcollection()->find(f_fav);
        if (fav) {
          opts->favorite = fav;
        } else {
          log::err << "invalid favorite name - " << f_fav << log::endl;
        }
      }
      if (f_fav_rule >= 0) {
        if (opts->favorite) {
          Favorite::iterator riter = opts->favorite->begin();
          int n = f_fav_rule;
          for(; n > 0 && riter != opts->favorite->end(); --n) ++riter;
          if (n != 0 || riter == opts->favorite->end()) {
            log::err << "rule number out of range" << log::endl;
          } else {
            opts->rule = *riter;
          }
        } else {
          log::err << "favorite not specified" << log::endl;
        }
      }
      if (!f_player.empty()) {
        opts->player = app_.conf().command()->players()->find(f_player);
      }

      if (!bc_root_.run(name, *iter_line, *opts)) {
        log::err << "failed to run built-in command - " << comm << log::endl;
      }
    } else {
      RefChannel ch;
      if (opts && opts->ch) ch = opts->ch;
      ProcessContext *pc = new ProcessContext(new Process(app_, type, line, comm, ch));
      pc->conn_dead_ = pc->proc_->signal_dead()
        .connect(sigc::bind(sigc::mem_fun(*this, &CommandRunner::process_on_dead), pc));
      pc->conn_error_ = pc->proc_->signal_error()
        .connect(sigc::bind(sigc::mem_fun(*this, &CommandRunner::process_on_error), pc));
      processes_.push_back(pc);
      signal_new_process_(pc->proc_);
      pc->proc_->run();
    }
  }
}

RefProcess CommandRunner::peercast() {
  return proc_peercast_;
}
RefProcess CommandRunner::pcraw_proxy() {
  return proc_pcraw_proxy_;
}

#define RUN_BUILTIN_COMMAND(NAME, OPTS)                 \
  run(*app_.conf().command()->NAME()->command(), OPTS);

bool CommandRunner::play(RefChannel ch, RefPlayer player) {
  if (!ch) return false;
  const ID &id = ch->get_id();
  if (!id) return false;

  ConfMain::RefPlayerCollection pc = app_.conf().command()->players();
  if (!player) player = pc->get_player(ch->get_type());
  if (!player) {
    log::err << "player is not specified" << log::endl;
    return false;
  }

  CommandOpts opts;
  opts.ch = ch;
  ch->set_played();
  signal_play_(ch);
  run(*player->command(), &opts, PROCESS_TYPE_PLAY);
  return true;
}
bool CommandRunner::auto_play(RefChannel ch, RefPlayer player) {
  if (!ch || ch->get_is_auto_played()) return false;
  const ID &id = ch->get_id();
  if (!id) return false;

  for(ProcessList::iterator iter = processes_.begin(); iter != processes_.end(); ++iter) {
    if (iter->proc_->get_type() == PROCESS_TYPE_PLAY) {
      RefChannel ch_p = iter->proc_->get_channel();
      if (ch_p && ch_p->get_id() == id) return false;
    }
  }

  ConfMain::RefPlayerCollection pc = app_.conf().command()->players();
  if (!player) player = pc->get_auto_player(ch->get_type());
  if (!player) player = pc->get_player(ch->get_type());
  if (!player) {
    log::err << "player is not specified" << log::endl;
    return false;
  }

  CommandOpts opts;
  opts.ch = ch;
  ch->set_played();
  ch->set_is_auto_played(true);
  signal_play_(ch);
  run(*player->command(), &opts, PROCESS_TYPE_PLAY);
  return true;
}

void CommandRunner::webbrowser(const Glib::ustring &url) {
  if (!url.empty()) {
    CommandOpts opts;
    opts.url = url.c_str();
    RUN_BUILTIN_COMMAND(webbrowser, &opts);
  }
}
void CommandRunner::bbsbrowser(const Glib::ustring &url) {
  if (!url.empty()) {
    CommandOpts opts;
    opts.url = url.c_str();
    RUN_BUILTIN_COMMAND(bbsbrowser, &opts);
  }
}

bool CommandRunner::record(RefChannel ch) {
  if (ch) {
    const ID &id = ch->get_id();
    if (id) {
      RecorderContext *rc = new RecorderContext(new Recorder(app_, ch));
      rc->conn_stop_ = rc->recorder_->signal_stop()
        .connect(sigc::bind(sigc::mem_fun(*this, &CommandRunner::recorder_on_stop), rc));
      recorders_.push_back(rc);
      signal_record_(rc->recorder_);
      rc->recorder_->start();
      return true;
    }
  }
  return false;
}
bool CommandRunner::auto_record(RefChannel ch) {
  ID id;
  if (!ch || !(id = ch->get_id())) return false;
  for(RecorderList::iterator iter = recorders_.begin(); iter != recorders_.end(); ++iter) {
    if (iter->recorder_->get_channel()->get_id() == id) return false;
  }
  return record(ch);
}

bool CommandRunner::bc_peercast(ShellLine &/*line*/, CommandOpts &/*opts*/) {
  run_peercast();
  return true;
}
bool CommandRunner::bc_pcraw_proxy(ShellLine &/*line*/, CommandOpts &/*opts*/) {
  run_pcraw_proxy();
  return true;
}
bool CommandRunner::bc_terminal(ShellLine &line, CommandOpts &opts) {
  opts.command = line.args_raw.c_str();
  RUN_BUILTIN_COMMAND(terminal, &opts);
  return true;
}

bool CommandRunner::bc_play(ShellLine &/*line*/, CommandOpts &opts) {
  return play(opts.ch, opts.player);
}

bool CommandRunner::bc_auto_play(ShellLine &/*line*/, CommandOpts &opts) {
  return auto_play(opts.ch, opts.player);
}
bool CommandRunner::bc_record(ShellLine &/*line*/, CommandOpts &opts) {
  return record(opts.ch);
}
bool CommandRunner::bc_auto_record(ShellLine &/*line*/, CommandOpts &opts) {
  return auto_record(opts.ch);
}
bool CommandRunner::bc_webbrowser(ShellLine &line, CommandOpts &opts) {
  if (line.args.size() >= 1) {
    const Glib::ustring &url = line.args.front();
    if (!url.empty()) {
      opts.url = url.c_str();
      RUN_BUILTIN_COMMAND(webbrowser, &opts);
      return true;
    }
  }
  return false;
}
bool CommandRunner::bc_bbsbrowser(ShellLine &line, CommandOpts &opts) {
  if (line.args.size() >= 1) {
    const Glib::ustring &url = line.args.front();
    if (!url.empty()) {
      opts.url = url.c_str();
      RUN_BUILTIN_COMMAND(bbsbrowser, &opts);
      return true;
    }
  }
  return false;
}
bool CommandRunner::bc_copy(ShellLine &line, CommandOpts &/*opts*/) {
  if (line.args.size()) {
    Glib::RefPtr<Gtk::Clipboard> clip = Gtk::Clipboard::get();
    clip->set_text(line.args.front());
    clip->store();
    return true;
  }
  return false;
}
bool CommandRunner::bc_request_url(ShellLine &line, CommandOpts &/*opts*/) {
  if (line.args.size()) {
    using namespace loader;
    URL url(line.args.front());
    Pool::request(url, Pool::SlotComplete(), Pool::SlotError())->load();
    return true;
  }
  return false;
}
bool CommandRunner::bc_quit(ShellLine &/*line*/, CommandOpts &/*opts*/) {
  Gtk::Main::quit();
  return true;
}

bool CommandRunner::bc_proc_terminate(ShellLine &/*line*/, CommandOpts &opts) {
  if (opts.proc) {
    opts.proc->terminate();
    return true;
  }
  return false;
}
bool CommandRunner::bc_proc_kill(ShellLine &/*line*/, CommandOpts &opts) {
  if (opts.proc) {
    opts.proc->kill();
    return true;
  }
  return false;
}
bool CommandRunner::bc_proc_signal(ShellLine &line, CommandOpts &opts) {
  if (opts.proc && line.args.size()) {
    int signal = toint(line.args.front(), -1);
    if (signal >= 0) {
      opts.proc->signal(signal);
      return true;
    }
  }
  return false;
}
bool CommandRunner::bc_proc_detach(ShellLine &/*line*/, CommandOpts &opts) {
  if (opts.proc) {
    Panel *panel = app_.mainwindow()->get_panel();
    if (panel) {
      ProcessView *pv = panel->page_processes();
      if (pv) {
        pv->detach(opts.proc);
        return true;
      }
    }
  }
  return false;
}

bool CommandRunner::bc_search_show(ShellLine &/*line*/, CommandOpts &opts) {
  if (opts.search) {
    opts.search->show();
    return true;
  }
  return false;
}
bool CommandRunner::bc_search_hide(ShellLine &/*line*/, CommandOpts &opts) {
  if (opts.search) {
    opts.search->hide();
    return true;
  }
  return false;
}
bool CommandRunner::bc_search_toggle(ShellLine &/*line*/, CommandOpts &opts) {
  if (opts.search) {
    if (opts.search->is_visible()) {
      opts.search->hide();
    } else {
      opts.search->show();
    }
    return true;
  }
  return false;
}
bool CommandRunner::bc_search_clear(ShellLine &/*line*/, CommandOpts &opts) {
  if (opts.search) {
    opts.search->clear();
    return true;
  }
  return false;
}

CommandRunner::CommandRunner(Application &app, BuiltinCommand &bc_root) :
  app_           (app),
  bc_root_       (bc_root),
  bc_proc_       (*bc_root_.add_ns("Process")),
  bc_search_     (*bc_root_.add_ns("Searchbar")),
  bc_text_editor_(*bc_root_.add_ns("TextEditor"))
{
#define ADD_CMD_F(BC, CMD, F, H)                                        \
  (BC).add_cmd(#CMD, sigc::mem_fun(*this, &CommandRunner::bc_##F), H)
#define ADD_CMD_G(CMD, H) ADD_CMD_F(bc_root_, CMD, CMD, H)
#define ADD_CMD(NS, CMD, H) ADD_CMD_F(bc_##NS##_, CMD, NS##_##CMD, H)

  ADD_CMD_G(peercast,      _("Run peercast"));
  ADD_CMD_G(pcraw_proxy,   _("Run pcraw_proxy"));
  ADD_CMD_G(terminal,      _("Run command in terminal"));
  ADD_CMD_G(play,          _("Play channel"));
  ADD_CMD_G(auto_play,     _("Play channel with auto-play command"));
  ADD_CMD_G(record,        _("Start recording"));
  ADD_CMD_G(auto_record,   _("Start auto-recording"));
  ADD_CMD_G(webbrowser,    _("Open URL"));
  ADD_CMD_G(bbsbrowser,    _("Open URL with 2ch browser"));
  ADD_CMD_G(copy,          _("Copy text to clipboard"));
  ADD_CMD_G(request_url,   _("Request URL"));
  ADD_CMD_G(quit,          _("Quit program"));

  ADD_CMD(proc, terminate, _("Send terminate signal"));
  ADD_CMD(proc, kill,      _("Send kill signal"));
  ADD_CMD(proc, signal,    _("Send signal"));
  ADD_CMD(proc, detach,    _("Detach process"));

  ADD_CMD(search, show,    _("Show searchbar"));
  ADD_CMD(search, hide,    _("Hide searchbar"));
  ADD_CMD(search, toggle,  _("Toggle searchbar"));
  ADD_CMD(search, clear,   _("Clear search-text"));

  Gtk::Main::signal_quit().connect(sigc::mem_fun(*this, &CommandRunner::main_on_quit));
}

}/*gpyp*/
